unit MasterDetailCli2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, rtcDB, Data.DB, Datasnap.DBClient,
  rtcFunction, rtcDataCli, rtcCliModule, rtcInfo, rtcConn, rtcHttpCli,
  Vcl.Grids, Vcl.DBGrids, Vcl.DBCtrls, Vcl.StdCtrls, Vcl.Buttons, Vcl.ExtCtrls;

type
  TForm3 = class(TForm)
    spl1: TSplitter;
    spl2: TSplitter;
    pnl1: TPanel;
    lbl1: TLabel;
    lbl2: TLabel;
    btnRefreshDataSet1: TSpeedButton;
    edtAddr: TEdit;
    edtPort: TEdit;
    chkValidCheck: TCheckBox;
    pnl2: TPanel;
    pnl3: TPanel;
    lbl3: TLabel;
    dbnvgr1: TDBNavigator;
    dbgrd1: TDBGrid;
    pnl4: TPanel;
    lblStatus: TLabel;
    pnl5: TPanel;
    spl3: TSplitter;
    pnl6: TPanel;
    pnl7: TPanel;
    lbl4: TLabel;
    dbnvgr2: TDBNavigator;
    pnl8: TPanel;
    btnRefreshDataSet3: TSpeedButton;
    dbgrd2: TDBGrid;
    pnl9: TPanel;
    pnl10: TPanel;
    lbl5: TLabel;
    dbnvgr3: TDBNavigator;
    pnl11: TPanel;
    btnRefreshDataSet2: TSpeedButton;
    dbgrd3: TDBGrid;
    rtchtpclnt1: TRtcHttpClient;
    rtclntmdl1: TRtcClientModule;
    rtcrslt1: TRtcResult;
    cds1: TClientDataSet;
    ds1: TDataSource;
    cds2: TClientDataSet;
    ds2: TDataSource;
    cds3: TClientDataSet;
    ds3: TDataSource;
    rtcdtstmntr1: TRtcDataSetMonitor;
    rtcdtstmntr2: TRtcDataSetMonitor;
    rtcdtstmntr3: TRtcDataSetMonitor;
    procedure FormCreate(Sender: TObject);
    procedure RtcMemDataSetDataChange(Sender: TObject);
    procedure rtcrslt1Return(Sender: TRtcConnection; Data, Result: TRtcValue);
    procedure rtcrslt1PreparingCall(Sender: TRtcConnection; Data,
      Result: TRtcValue);
    procedure rtcrslt1RequestAborted(Sender: TRtcConnection; Data,
      Result: TRtcValue);
    procedure btnRefreshDataSet2Click(Sender: TObject);
    procedure btnRefreshDataSet3Click(Sender: TObject);
    procedure cds1BeforeDelete(DataSet: TDataSet);
    procedure cds1BeforePost(DataSet: TDataSet);
    procedure cds1AfterScroll(DataSet: TDataSet);
    procedure cds2AfterScroll(DataSet: TDataSet);
    procedure cds2BeforeDelete(DataSet: TDataSet);
    procedure cds2BeforePost(DataSet: TDataSet);
    procedure cds2NewRecord(DataSet: TDataSet);
    procedure cds3NewRecord(DataSet: TDataSet);
    procedure cds3BeforePost(DataSet: TDataSet);
    procedure btnRefreshDataSet1Click(Sender: TObject);
  private
    { Private declarations }
  public
       CustNo,OrderNo:integer;
  end;

var
  Form3: TForm3;

implementation

{$R *.dfm}

procedure TForm3.btnRefreshDataSet1Click(Sender: TObject);
begin
  { Close Orders and Items DataSets, so the user can't make changes
    to unrelated Orders or Items while waiting for new Customer data ... }
  CustNo:=0;
  cds2.Active:=False;
  OrderNo:=0;
  cds2.Active:=False;

  { Closing the DataSet for which new data is being requested is NOT
    necessary, but it shows the user that new data is being loaded. }
  cds1.Active:=False;
  lblStatus.Caption:='Requesting Customer data';

  rtclntmdl1.Prepare('select');
  rtclntmdl1.Param.asText['table']:='customer';
  rtclntmdl1.Call(rtcrslt1, not assigned(Sender));
end;

procedure TForm3.btnRefreshDataSet2Click(Sender: TObject);
  begin
  { Close the Items DataSet, so the user can't make changes
    to unrelated Items while waiting for new Order data ... }
  OrderNo:=0;
  cds3.Active:=False;

  if CustNo<>0 then
    begin
    { Closing the DataSet for which new data is being requested is NOT
      necessary, but it shows the user that new data is being loaded. }
    cds2.Active:=False;
    lblStatus.Caption:='Requesting Orders data, CustNo='+IntToStr(CustNo);

    rtclntmdl1.Prepare('select');
    rtclntmdl1.Param.asText['table']:='orders';
    rtclntmdl1.Param.NewRecord('eq').asInteger['CustNo']:=CustNo;
    rtclntmdl1.Call(rtcrslt1);
    end
  else
    begin
    { There is no Customer info, close the Orders dataset. }
    cds2.Active:=False;
    lblStatus.Caption:='No Customer selected, Orders table closed.';
    end;
  end;

procedure TForm3.btnRefreshDataSet3Click(Sender: TObject);
  begin
  if OrderNo<>0 then
    begin
    { Closing the DataSet for which new data is being requested is NOT
      necessary, but it shows the user that new data is being loaded. }
    cds3.Active:=False;
    lblStatus.Caption:='Requesting Items data, OrderNo='+IntToStr(OrderNo);

    rtclntmdl1.Prepare('select');
    rtclntmdl1.Param.asText['table']:='items';
    rtclntmdl1.Param.NewRecord('eq').asInteger['OrderNo']:=OrderNo;
    rtclntmdl1.Call(rtcrslt1);
    end
  else
    begin
    { There is no Order info, close the Items dataset. }
    cds3.Active:=False;
    lblStatus.Caption:='No Order selected, Items table closed.';
    end;
  end;
procedure TForm3.cds1AfterScroll(DataSet: TDataSet);
  var
    NewNo:integer;
  begin
  { Customer data received, or the user has scrolled to a new Customer row.
    For our Master/Detail relation with Orders to work,
    we need to refresh our "Orders" data with a new "CustNo". }

  if cds1.Active and (cds1.RecordCount>0) then
    NewNo:=cds1.FieldByName('CustNo').AsInteger
  else
    NewNo:=0;

  if CustNo<>NewNo then
    begin
    CustNo:=NewNo;
    btnRefreshDataSet2Click(nil);
    end;
end;

procedure TForm3.cds1BeforeDelete(DataSet: TDataSet);
  begin
  if not chkValidCheck.Checked then Exit;

  { We can leave this job to the Server, but then the user will see
    the record deleted locally and then get an error from the Server. }
  if cds2.RecordCount>0 then
    raise Exception.Create('Can not delete Customer before deleting all Customer Orders.');
  end;

procedure TForm3.cds1BeforePost(DataSet: TDataSet);
  begin
  if not chkValidCheck.Checked then Exit;

  { We can leave this job to the Server, but then the user will
    have to refresh the dataset and repeat the last operation in
    case a field was undefined, so we will make this check here. }
  if cds1.FieldByName('CustNo').IsNull then
    raise Exception.Create('Required field "CustNo" is NULL');
  end;

procedure TForm3.cds2AfterScroll(DataSet: TDataSet);
  var
    NewNo:integer;
  begin
  { Orders data received, or the user has scrolled to a new Order row.
    For our Master/Detail relation with Items to work,
    we need to refresh our "Items" data with a new "OrderNo". }

  if cds2.Active and (cds2.RecordCount>0) then
    NewNo:=cds2.FieldByName('OrderNo').AsInteger
  else
    NewNo:=0;

  if OrderNo<>NewNo then
    begin
    OrderNo:=NewNo;
    btnRefreshDataSet3Click(nil);
    end;
  end;

procedure TForm3.cds2BeforeDelete(DataSet: TDataSet);
  begin
  if not chkValidCheck.Checked then Exit;

  { We can leave this job to the Server, but then the user will see
    the record deleted locally and then get an error from the Server. }
  if cds3.RecordCount>0 then
    raise Exception.Create('Can not delete Order before deleting all Order Items.');
  end;

procedure TForm3.cds2BeforePost(DataSet: TDataSet);
  begin
  if not chkValidCheck.Checked then Exit;

  { We can leave this job to the Server, but then the user will
    need to refresh the dataset and repeat the last operation in
    case a field is undefined, so we will make this check here. }
  if cds2.FieldByName('CustNo').AsInteger<>CustNo then
    raise Exception.Create('Required field "CustNo" has invalid value')
  else if cds2.FieldByName('OrderNo').IsNull then
    raise Exception.Create('Required field "OrderNo" is NULL')
  else if cds2.FieldByName('EmpNo').IsNull then
    raise Exception.Create('Required field "EmpNo" is NULL');
  end;

procedure TForm3.cds2NewRecord(DataSet: TDataSet);
  begin
  { We should set all "master" fields for a new Order here }
  cds2.FieldByName('CustNo').AsInteger:=CustNo;
  end;
procedure TForm3.cds3BeforePost(DataSet: TDataSet);
  begin
  if not chkValidCheck.Checked then Exit;

  { We can leave this job to the Server, but then the user will
    need to refresh the dataset and repeat the last operation in
    case a field is undefined, so we will make this check here. }
  if cds3.FieldByName('OrderNo').AsInteger<>OrderNo then
    raise Exception.Create('Required field "OrderNo" has invalid value')
  else if cds3.FieldByName('ItemNo').IsNull then
    raise Exception.Create('Required field "ItemNo" is NULL');
  end;

procedure TForm3.cds3NewRecord(DataSet: TDataSet);
  begin
  { We should set all "master" fields for a new Item here }
  cds3.FieldByName('OrderNo').AsInteger:=OrderNo;
  end;

procedure TForm3.FormCreate(Sender: TObject);
begin
  CustNo:=0; // Currently selected Customer
  OrderNo:=0; // Currently selected Order
end;

procedure TForm3.RtcMemDataSetDataChange(Sender: TObject);
  var
    data:TRtcValue;
    tblName:String;
  begin
  { We are using a single "OnDataChange" implementation for all DataSets }
  if Sender=rtchtpclnt1 then
    begin
    tblName:='customer';
    // Refresh Orders if Customer field value has changed
    cds1AfterScroll(nil);
    end
  else if Sender=rtcdtstmntr2 then
    begin
    tblName:='orders';
    // Refresh Items if Order field value was changed
    cds2AfterScroll(nil);
    end
  else if Sender=rtcdtstmntr3 then
    tblName:='items';

  data:=TRtcDataSetMonitor(Sender).ExtractChanges;
  if assigned(data) then
    begin
    lblStatus.Caption:='Sending '+tblName+' changes to the Server ...';
    rtclntmdl1.Prepare('submit');
    rtclntmdl1.Param.asText['table']:=tblName;
    rtclntmdl1.Param.asObject['change_data']:=data;
    rtclntmdl1.Call(rtcrslt1);
    end;
  end;


procedure TForm3.rtcrslt1PreparingCall(Sender: TRtcConnection; Data,
  Result: TRtcValue);
  var
    tblName:String;
  begin
  // Here, we can notify the user about each remote call just BEFORE it is sent to the Server,
  // and ... we can SKIP remote calls which are no longer relevant to the Client.
  tblName:=Data.asFunction.asText['table'];
  if Data.asFunction.FunctionName='select' then
    begin
    if tblName='customer' then
      lblStatus.Caption:='Requesting Customer data'
    else if tblName='orders' then
      begin
      if Data.asFunction.asRecord['eq'].asInteger['CustNo']<>CustNo then
        Data.isNull:=True // Skip this call, we have already moved away from that Customer row
      else
        lblStatus.Caption:='Requesting Orders data, CustNo='+
                          Data.asFunction.asRecord['eq'].asText['CustNo'];
      end
    else if tblName='items' then
      begin
      if Data.asFunction.asRecord['eq'].asInteger['OrderNo']<>OrderNo then
        Data.isNull:=True // Skip this call, we have already moved away from that Items row
      else
        lblStatus.Caption:='Requesting Items data, OrderNo='+
                          Data.asFunction.asRecord['eq'].asText['OrderNo'];
      end;
    end
  else if Data.asFunction.FunctionName='submit' then
    lblStatus.Caption:='Submitting changes for '+tblName;
  end;

procedure TForm3.rtcrslt1RequestAborted(Sender: TRtcConnection; Data,
  Result: TRtcValue);
  begin
  if Data.asFunction.FunctionName='select' then
    lblStatus.Caption:='Communication problems while sending a SELECT request to the Server'
  else if Data.asFunction.FunctionName='submit' then
    lblStatus.Caption:='Communication problems while submitting changes to the Server';
  end;


procedure TForm3.rtcrslt1Return(Sender: TRtcConnection; Data,
  Result: TRtcValue);
  var
    tblName:String;
    dts:TClientDataSet;
    mon:TRtcDataSetMonitor;
  begin
  tblName:=Data.asFunction.asText['table'];
  if Data.asFunction.FunctionName='select' then
    begin
    if Result.isType=rtc_DataSet then
      begin
      if tblName='customer' then
        begin
        dts:=cds1;
        mon:=rtcdtstmntr1;

        lblStatus.Caption:='Received Customer data';
        end
      else if tblName='orders' then
        begin
        dts:=cds2;
        mon:=rtcdtstmntr2;

        lblStatus.Caption:='Received Orders data, CustNo='+
                          Data.asFunction.asRecord['eq'].asText['CustNo'];
        { Because our requests are sent asynchronously,
          it is possible to receive a result for a master row
          after moving to a new master row. In that case,
          we should simply ignore the result. }
        if Data.asFunction.asRecord['eq'].asInteger['CustNo']<>CustNo then
          begin
          lblStatus.Caption:=lblStatus.Caption+', old (ignored).';
          Exit;
          end;
        end
      else if tblName='items' then
        begin
        dts:=cds3;
        mon:=rtcdtstmntr3;

        lblStatus.Caption:='Received Items data, OrderNo='+
                          Data.asFunction.asRecord['eq'].asText['OrderNo'];
        { Because our requests are sent asynchronously,
          it is possible to receive a result for a master row
          after moving to a new master row. In that case,
          we should simply ignore the result. }
        if Data.asFunction.asRecord['eq'].asInteger['OrderNo']<>OrderNo then
          begin
          lblStatus.Caption:=lblStatus.Caption+', old (ignored).';
          Exit;
          end;
        end
      else
        Exit; // Exit here to avoid a warning about undefined "dts" from the compiler (should never happen)

      mon.Active:=False;
      dts.DisableControls;
      try
        RtcDataSetFieldsToDelphi(Result.asDataSet, dts);
        dts.CreateDataSet;
        RtcDataSetRowsToDelphi(Result.asDataSet, dts);
      finally
        dts.EnableControls;
        mon.Active:=True;
        dts.First;
        end;
      end
    else
      begin
      if Result.isType=rtc_Exception then
        ShowMessage('Server-side exception after '+tblName+' Select:'+#13#10+Result.asException)
      else
        ShowMessage('Unexpected Result after '+tblName+' Select:'#13#10+Result.asCode);
      end;
    end
  else if Data.asFunction.FunctionName='submit' then
    begin
    if (Result.isType<>rtc_Boolean) or (Result.asBoolean<>TRUE) then
      begin
      if Result.isType=rtc_Exception then
        ShowMessage('Server-side exception after '+tblName+' Submit:'+#13#10+Result.asException)
      else
        ShowMessage('Unexpected Result after '+tblName+' Submit:'+#13#10+Result.asCode);
      end
    else
      lblStatus.Caption:='Changes to '+tblName+' submitted.';
    end;
  end;

end.
